package org.jetbrains.kotlin.idea.caches.project

import org.jetbrains.kotlin.platform.SimplePlatform
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.konan.NativePlatformUnspecifiedTarget
import org.jetbrains.kotlin.platform.konan.NativePlatformWithTarget

internal fun interface LibraryDependenciesFilter {
    operator fun invoke(platform: TargetPlatform, candidates: Set<LibraryDependencyCandidate>): Set<LibraryDependencyCandidate>
}

/**
 * Returns only candidates that 'support' all platforms aka. where the dependee's platform is a subset of the dependencies platforms.
 * jvm, js -> jvm, js, native {OK} (at least all platforms present)
 * jvm, js, native -> jvm, js {NO} (missing native platform)
 */
internal object DefaultLibraryDependenciesFilter : LibraryDependenciesFilter {
    override fun invoke(platform: TargetPlatform, candidates: Set<LibraryDependencyCandidate>): Set<LibraryDependencyCandidate> {
        return candidates.filterTo(mutableSetOf()) { candidate -> platform representsSubsetOf candidate.platform }
    }
}

/**
 * Returns only "fallback" libraries for shared native dependee libraries depending on interop libraries.
 * User projects might have less native targets than the libraries it is using. This is OK, however we therefore
 * do not generate the "perfect" commonization for this libraries.
 * We can still analyze this library (with more targets than our project) with the "next best" commonization at hand.
 * The "next best" commonization will give full coverage of all declarations that the library might use and is OK to leak into
 * source module analysis, since it is present there anyways (and by definition is the perfect fit for that source set)
 * see: https://youtrack.jetbrains.com/issue/KT-40814
 */
internal object SharedNativeLibraryToNativeInteropFallbackDependenciesFilter : LibraryDependenciesFilter {
    override fun invoke(platform: TargetPlatform, candidates: Set<LibraryDependencyCandidate>): Set<LibraryDependencyCandidate> {
        /* Filter only works on shared native dependee libraries to interop dependency libraries */
        if (!platform.isSharedNative()) return emptySet()

        return candidates.filterIsInstance<KlibLibraryDependencyCandidate>()
            .filter { it.isInterop && it.uniqueName != null }
            .groupBy { it.uniqueName }
            .flatMapTo(mutableSetOf()) { (_, candidates) ->
                val allCandidatePlatforms = candidates.mapTo(mutableSetOf()) { it.platform }
                val allCompatibleCandidatePlatforms = allCandidatePlatforms.filter { platform representsSubsetOf it }
                val allPlatformsCoveredByCandidates = allCompatibleCandidatePlatforms.any { it representsSubsetOf platform }
                if (allPlatformsCoveredByCandidates) return@flatMapTo emptyList()

                /* Not all platforms are covered. Carefully choose next best */
                val fallbackPermittedPlatform = allCandidatePlatforms
                    .filter { candidatePlatform -> candidatePlatform representsSubsetOf candidatePlatform }
                    .sortedWith(TargetPlatformComparator) // Guarantee a stable choice for "maxByOrNull"
                    .maxByOrNull { candidatePlatform -> candidatePlatform.componentPlatforms.size }

                candidates.filter { it.platform == fallbackPermittedPlatform }
            }
    }

    private object TargetPlatformComparator :
        Comparator<TargetPlatform> by compareBy<TargetPlatform>({ platform -> platform.componentPlatforms.size })
            .thenComparing({ platform -> platform.componentPlatforms.sortedWith(SimplePlatformComparator).joinToString() })

    private object SimplePlatformComparator :
        Comparator<SimplePlatform> by compareBy<SimplePlatform>({ it.targetName })
            .thenComparing<String>({ it.platformName })
}


/**
 * @return true if all platforms in [this@isCompatibleTo] have a compatible match in [to]
 *
 * e.g.
 * a, b, c -> a, b, c, d #true (all platforms available in "to")
 * a, b, c -> a, b #false (*not* all platforms available in "to")
 */
private infix fun TargetPlatform.representsSubsetOf(to: TargetPlatform): Boolean {
    return componentPlatforms.all { fromSimplePlatform ->
        to.componentPlatforms.any { toSimplePlatform -> fromSimplePlatform.representsSubsetOf(toSimplePlatform) }
    }
}

private fun SimplePlatform.representsSubsetOf(to: SimplePlatform): Boolean {
    return when {
        this == to -> true
        this is NativePlatformWithTarget && to is NativePlatformUnspecifiedTarget -> true
        else -> false
    }
}

/* Operators on LibraryDependenciesFilter */

internal infix fun LibraryDependenciesFilter.union(other: LibraryDependenciesFilter): LibraryDependenciesFilter {
    if (this is LibraryDependenciesFilterUnion && other is LibraryDependenciesFilterUnion) {
        return LibraryDependenciesFilterUnion(this.filters + other.filters)
    }
    if (this is LibraryDependenciesFilterUnion) {
        return LibraryDependenciesFilterUnion(filters + other)
    }
    if (other is LibraryDependenciesFilterUnion) {
        return LibraryDependenciesFilterUnion(listOf(this) + other.filters)
    }
    return LibraryDependenciesFilterUnion(listOf(this, other))
}

internal class LibraryDependenciesFilterUnion(
    val filters: List<LibraryDependenciesFilter>
) : LibraryDependenciesFilter {
    override fun invoke(platform: TargetPlatform, candidates: Set<LibraryDependencyCandidate>): Set<LibraryDependencyCandidate> {
        return filters
            .map { filter -> filter(platform, candidates) }
            .reduce { acc, set -> acc + set }
    }

    init {
        require(filters.isNotEmpty()) {
            "${this::class.java.simpleName} requires at least one filter"
        }
    }
}
